/**
 * Copyright (C) 2012 Daniel Gee.
 * See LICENSE.txt for more details.
 */
package com.rl.util;

import java.io.Serializable;
import java.util.List;
import java.util.Random;

/**
 * A class that can perform various random number generation techniques. Each instance of
 * <b>DiceRoller</b> keeps it's own {@link Random} instance, so several instances of this class can
 * be seeded and used along side each other in a more predictable way.
 * 
 * @author Daniel Gee
 */
public class DiceRoller implements Serializable
{
	/**
	 * For serialization.
	 */
	private static final long serialVersionUID = 8214482288628627857L;

	/**
	 * Compares a dice roll to a target number and determines the result level of the roll. A result
	 * level of 1 to 4 is a success, 0 is a failure, and -1 is also possible in the case of a very
	 * bad roll compared to the target number. For use with the {@link #step(int)} method if you
	 * want to have an idea of how successful or not a result was.
	 * 
	 * @param target
	 *            the number that you need to roll.
	 * @param result
	 *            the number that you did roll.
	 * @return the "level" of the result compared to the target.
	 * @see #step(int)
	 */
	public static int resultLevel(int target, int result)
	{
		/*
		 * The chart that these numbers are based off of is unfortunately lacking in patterns and
		 * predictability, so these are best fit equations of sorts.
		 */
		if (result >= 9 + (int) (target * 1.47))
		{
			return 4;
		}
		if (result >= 6 + (int) (target * 1.395))
		{
			return 3;
		}
		if (result >= 3 + (int) (target * 1.29))
		{
			return 2;
		}
		if (result >= target)
		{
			return 1;
		}
		if (result >= -1 + (int) (target * 0.67))
		{
			return 0;
		}
		return -1;
	}

	/**
	 * The random instance we'll be using for random numbers.
	 */
	private final Random r;

	/**
	 * Makes a new DiceRoller using its own {@link Random} instance.
	 */
	public DiceRoller()
	{
		this(new Random());
	}

	/**
	 * Makes a new DiceRoller that uses the provided {@link Random} instance.
	 * 
	 * @param r
	 *            the random instance to use.
	 */
	public DiceRoller(Random r)
	{
		this.r = r;
	}

	/**
	 * Returns a random boolean with the double given being the percent chance that the result is
	 * <tt>true</tt>. 0.75 is 75%, and so on. Chances less than 0.0 will always be <tt>false</tt>,
	 * and chances equal to 1.0 will always be <tt>true</tt>.
	 * 
	 * @param chance
	 *            how high the odds of <tt>true</tt> are.
	 * @return a random boolean.
	 */
	public boolean chance(double chance)
	{
		return this.r.nextDouble() < chance;
	}

	/**
	 * Returns a random boolean with the int given being the chance out of 100 that the result is
	 * <tt>true</tt>. Inputs of 0 or less will always result in <tt>false</tt>, while inputs of 100
	 * or more will always result in <tt>true</tt>.
	 * 
	 * @param chance
	 *            how high the odds of <tt>true</tt> are.
	 * @return a random boolean.
	 */
	public boolean chance(int chance)
	{
		return this.r.nextInt(100) < chance;
	}

	/**
	 * Returns a random boolean, with <tt>true</tt> being the result roughly <tt>times</tt> chances
	 * every <tt>outOf</tt> rolls. For example, a 3/8ths chance would be chance(3,8). No actual
	 * division is performed, so either value can be zero without problems. Inputs that would be a
	 * fraction less than 0.0 will always be <tt>false</tt>, and inputs that would be a fraction
	 * greater than 1.0 will always be <tt>true</tt>.
	 * 
	 * <p>
	 * <tt>outOf</tt> must be a non-negative value.
	 * </p>
	 * 
	 * @param times
	 *            how often the chance should be <tt>true</tt>.
	 * @param outOf
	 *            how often total.
	 * @return a random boolean.
	 * @throws IllegalArgumentException
	 *             if <tt>outOf</tt> is negative.
	 */
	public boolean chance(int times, int outOf)
	{
		if (outOf < 0.0)
		{
			throw new IllegalArgumentException("outOf must be a non-negative value, got: " + outOf);
		}
		return (int) (this.r.nextDouble() * outOf) < times;
	}

	/**
	 * Rolls the specified number of dice. Each die is +1, 0, or -1 (equal chance), and the total
	 * starts at 0. As you might expect, it gives a number that's bell curved around 0 regardless of
	 * the input size.
	 * 
	 * @param numDice
	 *            how many dice to roll. Less than 0 always results in 0.
	 * @return a number from -numDice to numDice, most likely close to 0.
	 */
	public int chocolate(int numDice)
	{
		int total = 0;
		while (numDice > 0)
		{
			// it's a bother to make -1, 0, or +1 with the RNG, just add 0 to 2 and decrement.
			total += this.r.nextInt(3);
			total--;
			numDice--;
		}
		return total;
	}

	/**
	 * Rolls several dice that all have the same number of sides, and totals up the results.
	 * 
	 * @param numDice
	 *            how many dice to roll. Less than 0 always results in 0.
	 * @param numSides
	 *            how many sides each die has. Must be a positive value.
	 * @return the total.
	 */
	public int dice(int numDice, int numSides)
	{
		if (numSides < 1)
		{
			throw new IllegalArgumentException("Cannot roll dice with less than 1 side, got: "
					+ numDice);
		}
		if (numDice < 0)
		{
			return 0;
		}
		int total = numDice;
		while (numDice > 0)
		{
			total += this.r.nextInt(numSides);
			numDice--;
		}
		return total;
	}

	/**
	 * Rolls a single die with <tt>numSides</tt> sides. If the result is a maximum result for that
	 * kind of die, another die is rolled and added to the total. That die can also generate another
	 * die, and so on. Because the bonus die always has a value of at least 1, a result exactly
	 * equal to the number of sides given isn't actually possible. For operations that require such
	 * a possible result, the {@link #hack(int, int)} funciton may be what you're looking for.
	 * 
	 * @param numSides
	 *            the number of sides this die roll will use. Must be greater than or equal to 2.
	 * @return the total.
	 * @throws IllegalArgumentException
	 *             if <tt>numSides</tt> is less than 2 (a 1-sided exploding die, while technically
	 *             possible, would be an infinite loop, so it's an error).
	 */
	public int explode(int numSides)
	{
		if (numSides < 2)
		{
			throw new IllegalArgumentException(
					"Cannot roll an exploding die with less than 2 sides, got:" + numSides);
		}
		int extraRollCounter = -1;
		int roll;
		do
		{
			extraRollCounter++;
			roll = (this.r.nextInt(numSides) + 1);
		}
		while (roll == numSides);
		return roll + extraRollCounter * numSides;
	}

	/**
	 * Rolls several dice, returning the total. For each die, if a maximum result is rolled, a bonus
	 * die of that type is added to the total along with a -1. So, unlike with {@link #explode(int)}
	 * , a result exactly equal to the sides rolled is possible (if the bonus die is only 1 it
	 * cancels with the -1). Dice expressions of this nature can have an 'h' appended to them to
	 * separate them from other expressions when writing them down (eg: 2d4h instead of 2d4). For
	 * reasons of not going too wildly outside the normal maximum, dice with 20 or more sides use
	 * 1d6h as their bonus die, and dice with 100 or more sides use 1d20h as their bonus die. Thus,
	 * you might roll 20 on 1d20h and then 5 on 1d6h (total of 24). Or you might roll 100 on 1d100h,
	 * 20 on 1d20h, 6 on 1d6h, and 3 on 1d6h (total of 126).
	 * 
	 * @param numDice
	 *            the number of dice to roll. Less than 0 always results in 0.
	 * @param numSides
	 *            the number of sides of each die. Must be greater than or equal to 2.
	 * @return a randomly rolled result.
	 * @throws IllegalArgumentException
	 *             if <tt>numSides</tt> is less than 2.
	 */
	public int hack(int numDice, int numSides)
	{
		if (numSides < 2)
		{
			throw new IllegalArgumentException("Hack dice must have at least 2 sides, got: "
					+ numSides);
		}
		if (numDice < 0)
		{
			return 0;
		}
		int total = numDice;
		int roll = this.r.nextInt(numSides);
		int bonusSides = numSides;
		if (numSides >= 100)
		{
			bonusSides = 20;
		}
		else if (numSides >= 20)
		{
			bonusSides = 6;
		}
		while (numDice > 0)
		{
			total += roll;
			if (roll == numSides)
			{
				total += this.hack(1, bonusSides) - 1;
			}
			roll = this.r.nextInt(numSides);
			numDice--;
		}
		return total;
	}

	/**
	 * Selects a random element out of an array of doubles.
	 * 
	 * @param array
	 *            the array to pick from.
	 * @return one of the items in the array.
	 */
	public double pickRandom(double[] array)
	{
		return array[(int) (this.r.nextDouble() * array.length)];
	}

	/**
	 * Selects a random element out of an array of objects.
	 * 
	 * @param array
	 *            the array to pick from.
	 * @return one of the items in the array.
	 */
	public <E> E pickRandom(E[] array)
	{
		return array[(int) (this.r.nextDouble() * array.length)];
	}

	/**
	 * Selects a random element out of an array of ints.
	 * 
	 * @param array
	 *            the array to pick from.
	 * @return one of the items in the array.
	 */
	public int pickRandom(int[] array)
	{
		return array[(int) (this.r.nextDouble() * array.length)];
	}

	/**
	 * Selects a random element out of a {@link List} of objects.
	 * 
	 * @param list
	 *            the list to select from.
	 * @return a random element from within the list.
	 */
	public <E> E pickRandom(List<E> list)
	{
		return list.get((int) (this.r.nextDouble() * list.size()));
	}

	/**
	 * Rolls many dice of the given <tt>numSides</tt>, then counts how many are equal to or greater
	 * than the target number (<tt>tn</tt>). Each die that's equal to or greater than the
	 * <tt>again</tt> number causes a bonus die to be rolled. If the rolling of bonus dice isn't
	 * desired, simply input a value for <tt>again</tt> that's greater than <tt>numSides</tt>. Bonus
	 * dice can also generate their own bonus dice.<br/>
	 * <br/>
	 * For example, an After Sundown roll of 10 dice would be <tt>pool(10,6,5,7)</tt>
	 * 
	 * @param numDice
	 *            the base number of dice to roll. If this is 0 or less the result will be 0.
	 * @param numSides
	 *            how many sides per die. This must be a positive value.
	 * @param tn
	 *            what a die needs to roll to add 1 to the total. If this is less than 1 the result
	 *            will be at equal to or greater than <tt>numDice</tt> (depending on <tt>again</tt>)
	 *            (unless numDice is less than 0, in which case it will still be 0).
	 * @param again
	 *            what a die needs to roll to generate a bonus die. This must be at least 2.
	 * @return the total.
	 * @throws IllegalArgumentException
	 *             if numSides is less than 1.
	 * @throws IllegalArgumentException
	 *             if <tt>again</tt> is less than 2.
	 */
	public int pool(int numDice, int numSides, int tn, int again)
	{
		if (numSides < 1)
		{
			throw new IllegalArgumentException("numSides must be greater than 0, got: " + numSides);
		}
		if (again < 2)
		{
			throw new IllegalArgumentException("again must be greater than 1, got: " + again);
		}
		int total = 0;
		int roll;
		while (numDice > 0)
		{
			roll = this.r.nextInt(numSides) + 1;
			if (roll >= tn)
			{
				total++;
			}
			if (roll >= again)
			{
				continue;
			}
			numDice--;
		}
		return total;
	}

	/**
	 * Returns a number in the range given, inclusive at both ends.
	 * 
	 * @param min
	 *            the lowest possible result.
	 * @param max
	 *            the highest possible result.
	 * @return a random number.
	 */
	public int randInRange(int min, int max)
	{
		if (min > max)
		{
			throw new IllegalArgumentException(String.format(
					"The min value can't be greater than the max value, got: [%d,%d]", min, max));
		}
		long diff = ((long) max) - ((long) min);
		return (int) (min + Math.random() * (diff + 1));
	}

	/**
	 * Returns a value with a minimum of 1 and an exponentially falling chance of being above 1,
	 * with a maximum possbile value of <tt>level</tt> divided by 3 (though the maximum is at least
	 * 5, even when level is below 15).
	 * 
	 * @param x
	 *            controls the odds of the return being the next higher number each loop, 1/x style.
	 *            2 is 1/2, 3 is 1/3, etc.
	 * @param level
	 *            the level of the character, influences the maximum possible output.
	 * @return a number from 1 to the level-based maximum (inclusive).
	 * @throws IllegalArgumentException
	 *             if x is less than 1.
	 */
	public int rnExponent(int x, int level)
	{
		if (x < 1 || level < 1)
		{
			throw new IllegalArgumentException("inputs must be positive, got: " + x + "," + level);
		}
		int limit = (level < 15) ? 5 : level / 3;
		int total = 1;
		while (total < limit && this.r.nextInt(x) == 0)
		{
			total++;
		}
		return total;
	}

	/**
	 * Returns a number that's lower with higher luck, and higher with lower luck. The minimum is 0,
	 * and the maximum is x-1. The absolute value of the luck variable probably shouldn't be more
	 * than 13, and luck cannot be greater than 49 or there will be an error (though it could be
	 * less than -49). The idea is that this is called when testing if a curse or other "bad thing"
	 * might happen; Any non-zero result indicates that it does happen and a result of 0 indicates
	 * that the character is fine.
	 * 
	 * @param x
	 *            the upper limit of the output.
	 * @param luck
	 *            the character's luck score.
	 * @return a luck influenced value from 0 to x-1.
	 */
	public int rnLuck(int x, int luck)
	{
		if (x < 1)
		{
			throw new IllegalArgumentException("x must be positive, got:" + x);
		}
		if (Math.abs(luck) >= 50)
		{
			throw new IllegalArgumentException("luck's absolute value must be less than 50, got: "
					+ luck);
		}
		int i = this.r.nextInt(x);
		int bitMask = this.r.nextInt(50 - luck);
		if ((luck & bitMask) > 0)
		{
			if (x <= 15 && luck >= -5)
			{
				return Math.min(Math.max(0, i - (luck / 3)), x - 1);
			}
			else
			{
				return Math.min(Math.max(0, i - luck), x - 1);
			}
		}
		else
		{
			return i;
		}
	}

	/**
	 * Returns a really weird number that's often half the input, unless you're around 350 where
	 * it's around the input. The output drops off again as you go above 350.
	 * 
	 * @param i
	 *            an input value.
	 * @param level
	 *            the character's experience level, a call to {@link #rnExponent(int, int)} is used.
	 * @return a randomized value.
	 * @throws IllegalArgumentException
	 *             if <tt>i</tt> is less than 1 or greater than 100,000
	 */
	public int rnZ(int i, int level)
	{
		if (i < 1 || i > 100000)
		{
			throw new IllegalArgumentException("i must be from 1 to 100,000 or more, got: " + i);
		}
		int tmp = 1000;
		tmp += this.r.nextInt(1000);
		tmp *= this.rnExponent(4, level);
		if (this.r.nextInt(2) == 0)
		{
			i *= tmp;
			i /= 1000;
		}
		else
		{
			i *= 1000;
			i /= tmp;
		}
		return i;
	}

	/**
	 * Assigns a specific seed value to this DiceRoller's instance of {@link Random}.
	 * 
	 * @param seed
	 *            the seed to use.
	 * @see Random#setSeed(long)
	 */
	public void setSeed(long seed)
	{
		this.r.setSeed(seed);
	}

	/**
	 * Rolls the exploding dice for the step value input. The average output is equal to the input,
	 * but the actual result can vary wildly, and there is no strict upper limit. A step value of
	 * less than 1 gives a result of 0. In all other cases the minimum value is
	 * <tt>(value / 7 + 1)</tt>, with no upper maximum. As the minimum value increases, the results
	 * become increasingly bell curved, because the minimum is 1 for each die rolled in the process.
	 * 
	 * @param value
	 *            the step value to roll.
	 * @return a random result which averages close to the input value.
	 * @see #resultLevel(int, int)
	 */
	public int step(int value)
	{
		if (value < 1)
		{
			return 0;
		}
		switch (value)
		{
		case 1:
			return Math.max(this.explode(6) - 3, 1);
		case 2:
			return Math.max(this.explode(6) - 2, 1);
		case 3:
			return Math.max(this.explode(6) - 1, 1);
		case 4:
			return this.explode(6);
		case 5:
			return this.explode(8);
		case 6:
			return this.explode(10);
		case 7:
			return this.explode(12);
		case 8:
			return this.explode(6) + this.explode(6);
		case 9:
			return this.explode(6) + this.explode(8);
		case 10:
			return this.explode(8) + this.explode(8);
		case 11:
			return this.explode(8) + this.explode(10);
		case 12:
			return this.explode(10) + this.explode(10);
		case 13:
			return this.explode(10) + this.explode(12);
		default:
			int total = 0;
			while (value > 13)
			{
				total += this.step(12);
				value -= 7;
			}
			total += this.step(value);
			return total;
		}
	}

	/**
	 * Rolls many dice with a given number of sides and returns the highest result. If more than one
	 * dice result in the maximum possible result (eg: a 20 on 1d20), then +1 is added to the result
	 * for each maximum beyond the first. For example, using four d6s, if {1, 3, 4, 2} was rolled,
	 * then the result would be 4. If {4, 6, 6, 1} was rolled the result would be 7 (the 6, with +1
	 * for an additional 6).
	 * 
	 * @param numDice
	 *            how many dice to roll.
	 * @param numSides
	 *            how many sides to roll.
	 * @return a dice result weighted towards higher numbers.
	 */
	public int takeHighest(int numDice, int numSides)
	{
		if (numSides < 1)
		{
			throw new IllegalArgumentException("Cannot roll dice with less than 1 side, got: "
					+ numDice);
		}
		int total = 0;
		int roll;
		while (numDice > 0)
		{
			roll = 1 + this.r.nextInt(numSides);
			if (total < numSides)
			{
				total = Math.max(total, roll);
			}
			else
			{
				if (roll == numSides)
				{
					total++;
				}
			}
			numDice--;
		}
		return total;
	}
}
